/*
 ------------------------------------------------------------------------------
 Copyright (C) 2006-2007 Team Blur.

 This file is part of the Quantum Engine source code.

 The Quantum Engine source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Quantum Engine source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Quantum Engine source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


#include "r_local.h"


#define TEXTURES_HASH_SIZE				MAX_TEXTURES / 4

typedef struct {
	const char *		suffix;
	bool				flipX;
	bool				flipY;
	bool				flipDiagonal;
} cubeSide_t;

typedef struct {
	const char *		suffix;
	odAngles			angles;
	bool				flipX;
	bool				flipY;
	bool				flipDiagonal;
} envShot_t;

typedef struct {
	const char *		name;
	int					min;
	int					mag;
} filterTable_t;

static cubeSide_t		r_cubeSides[2][6] = {
	{
		{"px", false, false, false},
		{"nx", false, false, false},
		{"py", false, false, false},
		{"ny", false, false, false},
		{"pz", false, false, false},
		{"nz", false, false, false}
	},
	{
		{"rt", false, false, true },
		{"lf", true , true , true },
		{"bk", false, true , false},
		{"ft", true , false, false},
		{"up", false, false, true },
		{"dn", false, false, true }
	}
};

static envShot_t		r_envShot[6] = {
	{"px", odAngles(   0.0f,   0.0f,  0.0f), true , true , true },
	{"nx", odAngles(   0.0f, 180.0f,  0.0f), false, false, true },
	{"py", odAngles(   0.0f,  90.0f,  0.0f), false, true , false},
	{"ny", odAngles(   0.0f, 270.0f,  0.0f), true , false, false},
	{"pz", odAngles( -90.0f, 180.0f,  0.0f), false, false, true },
	{"nz", odAngles(  90.0f, 180.0f,  0.0f), false, false, true }
};

static filterTable_t	r_filterTable[] = {
	{"GL_NEAREST",					GL_NEAREST,					GL_NEAREST},
	{"GL_LINEAR",					GL_LINEAR,					GL_LINEAR},
	{"GL_NEAREST_MIPMAP_NEAREST",	GL_NEAREST_MIPMAP_NEAREST,	GL_NEAREST},
	{"GL_LINEAR_MIPMAP_NEAREST",	GL_LINEAR_MIPMAP_NEAREST,	GL_LINEAR},
	{"GL_NEAREST_MIPMAP_LINEAR",	GL_NEAREST_MIPMAP_LINEAR,	GL_NEAREST},
	{"GL_LINEAR_MIPMAP_LINEAR",		GL_LINEAR_MIPMAP_LINEAR,	GL_LINEAR},
	{NULL,							0,							0}
};

static float			r_luminanceTable[256][3];

static int				r_textureMinFilter = GL_LINEAR_MIPMAP_LINEAR;
static int				r_textureMagFilter = GL_LINEAR;

static texture_t *		r_texturesHashTable[TEXTURES_HASH_SIZE];
static texture_t *		r_textures[MAX_TEXTURES];
static int				r_numTextures;


/*
 ==============================================================================

 TGA LOADING

 ==============================================================================
*/


/*
 ==================
 R_LoadTGA
 ==================
*/
static bool R_LoadTGA (const char *name, byte **image, int *width, int *height){

	tgaHeader_t	header;
	byte		*data;
	byte		*in, *out;
	int			x, y, stride;

	// Load the file
	FS_ReadFile(name, (void **)&data);
	if (!data)
		return false;

	// Parse the TGA header
	header.idLength = data[0];
	header.colormapType = data[1];
	header.imageType = data[2];
	header.colormapIndex = data[3] | (data[4] << 8);
	header.colormapLength = data[5] | (data[6] << 8);
	header.colormapSize = data[7];
	header.xOrigin = data[8] | (data[9] << 8);
	header.yOrigin = data[10] | (data[11] << 8);
	header.width = data[12] | (data[13] << 8);
	header.height = data[14] | (data[15] << 8);
	header.pixelSize = data[16];
	header.attributes = data[17];

	if (header.imageType != 2 && header.imageType != 3)
		Com_Error(false, "R_LoadTGA: only type 2 (RGB/RGBA) and 3 (gray) images supported (%s)", name);

	if (header.colormapType != 0)
		Com_Error(false, "R_LoadTGA: colormaps not supported (%s)", name);
		
	if (header.pixelSize != 8 && header.pixelSize != 24 && header.pixelSize != 32)
		Com_Error(false, "R_LoadTGA: only 8 (gray), 24 (RGB) and 32 (RGBA) bit images supported (%s)", name);

	if (header.width == 0 || header.height == 0 || header.width > 4096 || header.height > 4096)
		Com_Error(false, "R_LoadTGA: bad image size (%i x %i) (%s)", header.width, header.height, name);

	// Read the image pixels
	in = data + 18 + header.idLength;

	if (header.attributes & 0x20)
		stride = 0;
	else {
		// Flipped image
		stride = -header.width * (header.pixelSize >> 3) * 2;

		in += (header.height - 1) * header.width * (header.pixelSize >> 3);
	}

	*image = out = (byte *)Mem_Alloc(header.width * header.height * 4, TAG_TEMP);

	*width = header.width;
	*height = header.height;

	switch (header.pixelSize){
	case 8:
		for (y = 0; y < header.height; y++, in += stride){
			for (x = 0; x < header.width; x++, in += 1, out += 4){
				out[0] = in[0];
				out[1] = in[0];
				out[2] = in[0];
				out[3] = 255;
			}
		}

		break;
	case 24:
		for (y = 0; y < header.height; y++, in += stride){
			for (x = 0; x < header.width; x++, in += 3, out += 4){
				out[0] = in[2];
				out[1] = in[1];
				out[2] = in[0];
				out[3] = 255;
			}
		}

		break;
	case 32:
		for (y = 0; y < header.height; y++, in += stride){
			for (x = 0; x < header.width; x++, in += 4, out += 4){
				out[0] = in[2];
				out[1] = in[1];
				out[2] = in[0];
				out[3] = in[3];
			}
		}

		break;
	}

	// Free file data
	FS_FreeFile(data);

	return true;
}


/*
 ==============================================================================

 IMAGE PROGRAM FUNCTIONS

 ==============================================================================
*/

static bool	R_LoadImage (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp);


/*
 ==================
 R_FlipImage

 Flips the given image
 ==================
*/
static byte *R_FlipImage (byte *in, int width, int height, int samples, bool flipX, bool flipY, bool flipDiagonal){

	byte	*image, *out;
	byte	*line, *l;
	int		xStride, yStride;
	int		xOffset, yOffset;
	int		x, y;
	int		i;

	image = out = (byte *)Mem_Alloc(width * height * samples, TAG_TEMP);

	if (flipX){
		xStride = -samples;
		xOffset = (width - 1) * samples;
	}
	else {
		xStride = samples;
		xOffset = 0;
	}

	if (flipY){
		yStride = -samples * width;
		yOffset = (height - 1) * width * samples;
	}
	else {
		yStride = samples * width;
		yOffset = 0;
	}

	if (flipDiagonal){
		for (x = 0, line = in + xOffset; x < width; x++, line += xStride){
			for (y = 0, l = line + yOffset; y < height; y++, l += yStride, out += samples){
				for (i = 0; i < samples; i++)
					out[i] = l[i];
			}
		}
	}
	else {
		for (y = 0, line = in + yOffset; y < height; y++, line += yStride){
			for (x = 0, l = line + xOffset; x < width; x++, l += xStride, out += samples){
				for (i = 0; i < samples; i++)
					out[i] = l[i];
			}
		}
	}

	return image;
}

/*
 ==================
 R_AddImages

 Adds the given images together
 ==================
*/
static byte *R_AddImages (const byte *in1, const byte *in2, int width, int height){

	byte	*image, *out;
	int		x, y;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, in1 += 4, in2 += 4, out += 4){
			out[0] = M_ClampByte(in1[0] + in2[0]);
			out[1] = M_ClampByte(in1[1] + in2[1]);
			out[2] = M_ClampByte(in1[2] + in2[2]);
			out[3] = M_ClampByte(in1[3] + in2[3]);
		}
	}

	return image;
}

/*
 ==================
 R_ScaleImage

 Scales the given image
 ==================
*/
static byte *R_ScaleImage (const byte *in, int width, int height, const odVec4 &scale){

	byte	*image, *out;
	int		x, y;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, in += 4, out += 4){
			out[0] = M_ClampByte(M_Ftol(in[0] * scale[0]));
			out[1] = M_ClampByte(M_Ftol(in[1] * scale[1]));
			out[2] = M_ClampByte(M_Ftol(in[2] * scale[2]));
			out[3] = M_ClampByte(M_Ftol(in[3] * scale[3]));
		}
	}

	return image;
}

/*
 ==================
 R_InvertImageColor

 Inverts the color channels of the given image
 ==================
*/
static byte *R_InvertImageColor (const byte *in, int width, int height){

	byte	*image, *out;
	int		x, y;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, in += 4, out += 4){
			out[0] = 255 - in[0];
			out[1] = 255 - in[1];
			out[2] = 255 - in[2];
			out[3] = in[3];
		}
	}

	return image;
}

/*
 ==================
 R_InvertImageAlpha

 Inverts the alpha channel of the given image
 ==================
*/
static byte *R_InvertImageAlpha (const byte *in, int width, int height){

	byte	*image, *out;
	int		x, y;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, in += 4, out += 4){
			out[0] = in[0];
			out[1] = in[1];
			out[2] = in[2];
			out[3] = 255 - in[3];
		}
	}

	return image;
}

/*
 ==================
 R_MakeImageIntensity

 Converts the given image to intensity
 ==================
*/
static byte *R_MakeImageIntensity (const byte *in, int width, int height){

	byte	*image, *out;
	float	r, g, b;
	int		intensity;
	int		x, y;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, in += 4, out += 4){
			r = r_luminanceTable[in[0]][0];
			g = r_luminanceTable[in[1]][1];
			b = r_luminanceTable[in[2]][2];

			intensity = M_Ftol(r + g + b);

			out[0] = intensity;
			out[1] = intensity;
			out[2] = intensity;
			out[3] = intensity;
		}
	}

	return image;
}

/*
 ==================
 R_MakeImageLuminance

 Converts the given image to luminance
 ==================
*/
static byte *R_MakeImageLuminance (const byte *in, int width, int height){

	byte	*image, *out;
	float	r, g, b;
	int		luminance;
	int		x, y;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, in += 4, out += 4){
			r = r_luminanceTable[in[0]][0];
			g = r_luminanceTable[in[1]][1];
			b = r_luminanceTable[in[2]][2];

			luminance = M_Ftol(r + g + b);

			out[0] = luminance;
			out[1] = luminance;
			out[2] = luminance;
			out[3] = 255;
		}
	}

	return image;
}

/*
 ==================
 R_MakeImageAlpha

 Converts the given image to alpha
 ==================
*/
static byte *R_MakeImageAlpha (const byte *in, int width, int height){

	byte	*image, *out;
	float	r, g, b;
	int		alpha;
	int		x, y;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, in += 4, out += 4){
			r = r_luminanceTable[in[0]][0];
			g = r_luminanceTable[in[1]][1];
			b = r_luminanceTable[in[2]][2];

			alpha = M_Ftol(r + g + b);

			out[0] = 255;
			out[1] = 255;
			out[2] = 255;
			out[3] = alpha;
		}
	}

	return image;
}

/*
 ==================
 R_HeightMapToNormalMap

 Converts the given height map to a normal map
 ==================
*/
static byte *R_HeightMapToNormalMap (const byte *in, int width, int height, float scale, bool clamp){

	byte	*image, *out;
	float	r, g, b;
	float	c, cx, cy;
	odVec3	normal;
	int		x, y;
	int		i;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, out += 4){
			i = (y * width + x) * 4;

			r = r_luminanceTable[in[i+0]][0];
			g = r_luminanceTable[in[i+1]][1];
			b = r_luminanceTable[in[i+2]][2];

			c = (r + g + b) * (1.0f/255);

			if (clamp && (x == width - 1))
				i = (y * width + x) * 4;
			else
				i = (y * width + ((x+1) % width)) * 4;

			r = r_luminanceTable[in[i+0]][0];
			g = r_luminanceTable[in[i+1]][1];
			b = r_luminanceTable[in[i+2]][2];

			cx = (r + g + b) * (1.0f/255);

			if (clamp && (y == height - 1))
				i = (y * width + x) * 4;
			else
				i = (((y+1) % height) * width + x) * 4;

			r = r_luminanceTable[in[i+0]][0];
			g = r_luminanceTable[in[i+1]][1];
			b = r_luminanceTable[in[i+2]][2];

			cy = (r + g + b) * (1.0f/255);

			normal.Set((c - cx) * scale, (c - cy) * scale, 1.0f);

			if (!normal.Normalize())
				normal.Set(0.0f, 0.0f, 1.0f);

			out[0] = 128 + M_Ftol(127.0f * normal[0]);
			out[1] = 128 + M_Ftol(127.0f * normal[1]);
			out[2] = 128 + M_Ftol(127.0f * normal[2]);
			out[3] = 255;
		}
	}

	return image;
}

/*
 ==================
 R_AddNormalMaps

 Adds the given normal maps together
 ==================
*/
static byte *R_AddNormalMaps (const byte *in1, const byte *in2, int width, int height){

	byte	*image, *out;
	odVec3	normal;
	int		x, y;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, in1 += 4, in2 += 4, out += 4){
			normal[0] = (in1[0] * (1.0f/127) - 1.0f) + (in2[0] * (1.0f/127) - 1.0f);
			normal[1] = (in1[1] * (1.0f/127) - 1.0f) + (in2[1] * (1.0f/127) - 1.0f);
			normal[2] = (in1[2] * (1.0f/127) - 1.0f) + (in2[2] * (1.0f/127) - 1.0f);

			if (!normal.Normalize())
				normal.Set(0.0f, 0.0f, 1.0f);

			out[0] = 128 + M_Ftol(127.0f * normal[0]);
			out[1] = 128 + M_Ftol(127.0f * normal[1]);
			out[2] = 128 + M_Ftol(127.0f * normal[2]);
			out[3] = 255;
		}
	}

	return image;
}

/*
 ==================
 R_SmoothNormalMap

 Smoothes the given normal map
 ==================
*/
static byte *R_SmoothNormalMap (const byte *in, int width, int height, bool clamp){

	byte	*image, *out;
	int		xOffsets[3], yOffsets[3];
	int		xOfs, yOfs;
	odVec3	normal;
	int		x, y;
	int		i, j;

	image = out = (byte *)Mem_Alloc(width * height * 4, TAG_TEMP);

	for (y = 0; y < height; y++){
		for (x = 0; x < width; x++, out += 4){
			normal.Clear();

			xOffsets[0] = x;
			xOffsets[1] = (x != 0) ? x - 1 : (clamp) ? -1 : width - 1;
			xOffsets[2] = (x != width - 1) ? x + 1 : (clamp) ? -1 : 0;

			yOffsets[0] = y;
			yOffsets[1] = (y != 0) ? y - 1 : (clamp) ? -1 : height - 1;
			yOffsets[2] = (y != height - 1) ? y + 1 : (clamp) ? -1 : 0;

			for (j = 0; j < 9; j++){
				xOfs = xOffsets[j / 3];
				yOfs = yOffsets[j % 3];

				if (xOfs == -1 || yOfs == -1)
					continue;

				i = (yOfs * width + xOfs) * 4;

				normal[0] += (in[i+0] * (1.0f/127) - 1.0f);
				normal[1] += (in[i+1] * (1.0f/127) - 1.0f);
				normal[2] += (in[i+2] * (1.0f/127) - 1.0f);
			}

			if (!normal.Normalize())
				normal.Set(0.0f, 0.0f, 1.0f);

			out[0] = 128 + M_Ftol(127.0f * normal[0]);
			out[1] = 128 + M_Ftol(127.0f * normal[1]);
			out[2] = 128 + M_Ftol(127.0f * normal[2]);
			out[3] = 255;
		}
	}

	return image;
}

/*
 ==================
 R_ParseAdd
 ==================
*/
static bool R_ParseAdd (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img1, *img2;
	int		width1, height1, width2, height2;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img1, &width1, &height1, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ",")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ',', found '%s' instead in image program\n", token.string);

		Mem_Free(img1);
		return false;
	}

	if (!R_LoadImage(script, &img2, &width2, &height2, flags, clamp)){
		Mem_Free(img1);
		return false;
	}

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img1);
		Mem_Free(img2);
		return false;
	}

	if (width1 != width2 || height1 != height2){
		Com_Printf(S_COLOR_YELLOW "WARNING: images have mismatched dimensions (%i x %i != %i x %i)\n", width1, height1, width2, height2);

		Mem_Free(img1);
		Mem_Free(img2);
		return false;
	}

	*width = width1;
	*height = height1;

	*flags &= ~(TF_INTENSITY | TF_ALPHA);

	*image = R_AddImages(img1, img2, *width, *height);

	Mem_Free(img1);
	Mem_Free(img2);

	return true;
}

/*
 ==================
 R_ParseScale
 ==================
*/
static bool R_ParseScale (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img;
	odVec4	scale;
	int		i;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img, width, height, flags, clamp))
		return false;

	for (i = 0; i < 4; i++){
		if (!PS_ExpectTokenString(script, &token, ",")){
			Com_Printf(S_COLOR_YELLOW "WARNING: expected ',', found '%s' instead in image program\n", token.string);

			Mem_Free(img);
			return false;
		}

		if (!PS_ReadFloat(script, &scale[i])){
			Com_Printf(S_COLOR_YELLOW "WARNING: missing parameters for image program\n");

			Mem_Free(img);
			return false;
		}
	}

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	*flags &= ~(TF_INTENSITY | TF_ALPHA);

	*image = R_ScaleImage(img, *width, *height, scale);

	Mem_Free(img);

	return true;
}

/*
 ==================
 R_ParseInvertColor
 ==================
*/
static bool R_ParseInvertColor (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img, width, height, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	*image = R_InvertImageColor(img, *width, *height);

	Mem_Free(img);

	return true;
}

/*
 ==================
 R_ParseInvertAlpha
 ==================
*/
static bool R_ParseInvertAlpha (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img, width, height, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	*image = R_InvertImageAlpha(img, *width, *height);

	Mem_Free(img);

	return true;
}

/*
 ==================
 R_ParseMakeIntensity
 ==================
*/
static bool R_ParseMakeIntensity (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img, width, height, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	*flags |= TF_INTENSITY;
	*flags &= ~(TF_ALPHA | TF_NORMALMAP);

	*image = R_MakeImageIntensity(img, *width, *height);

	Mem_Free(img);

	return true;
}

/*
 ==================
 R_ParseMakeLuminance
 ==================
*/
static bool R_ParseMakeLuminance (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img, width, height, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	*flags &= ~(TF_INTENSITY | TF_ALPHA | TF_NORMALMAP);

	*image = R_MakeImageLuminance(img, *width, *height);

	Mem_Free(img);

	return true;
}

/*
 ==================
 R_ParseMakeAlpha
 ==================
*/
static bool R_ParseMakeAlpha (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img, width, height, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	*flags |= TF_ALPHA;
	*flags &= ~(TF_INTENSITY | TF_NORMALMAP);

	*image = R_MakeImageAlpha(img, *width, *height);

	Mem_Free(img);

	return true;
}

/*
 ==================
 R_ParseHeightMap
 ==================
*/
static bool R_ParseHeightMap (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img;
	float	scale;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img, width, height, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ",")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ',', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	if (!PS_ReadFloat(script, &scale)){
		Com_Printf(S_COLOR_YELLOW "WARNING: missing parameters for image program\n");

		Mem_Free(img);
		return false;
	}

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	*flags |= TF_NORMALMAP;
	*flags &= ~(TF_INTENSITY | TF_ALPHA);

	*image = R_HeightMapToNormalMap(img, *width, *height, scale, clamp);

	Mem_Free(img);

	return true;
}

/*
 ==================
 R_ParseAddNormals
 ==================
*/
static bool R_ParseAddNormals (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img1, *img2;
	int		width1, height1, width2, height2;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img1, &width1, &height1, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ",")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ',', found '%s' instead in image program\n", token.string);

		Mem_Free(img1);
		return false;
	}

	if (!R_LoadImage(script, &img2, &width2, &height2, flags, clamp)){
		Mem_Free(img1);
		return false;
	}

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img1);
		Mem_Free(img2);
		return false;
	}

	if (width1 != width2 || height1 != height2){
		Com_Printf(S_COLOR_YELLOW "WARNING: images have mismatched dimensions (%i x %i != %i x %i)\n", width1, height1, width2, height2);

		Mem_Free(img1);
		Mem_Free(img2);
		return false;
	}

	*width = width1;
	*height = height1;

	*flags |= TF_NORMALMAP;
	*flags &= ~(TF_INTENSITY | TF_ALPHA);

	*image = R_AddNormalMaps(img1, img2, *width, *height);

	Mem_Free(img1);
	Mem_Free(img2);

	return true;
}

/*
 ==================
 R_ParseSmoothNormals
 ==================
*/
static bool R_ParseSmoothNormals (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;
	byte	*img;

	if (!PS_ExpectTokenString(script, &token, "(")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected '(', found '%s' instead in image program\n", token.string);
		return false;
	}

	if (!R_LoadImage(script, &img, width, height, flags, clamp))
		return false;

	if (!PS_ExpectTokenString(script, &token, ")")){
		Com_Printf(S_COLOR_YELLOW "WARNING: expected ')', found '%s' instead in image program\n", token.string);

		Mem_Free(img);
		return false;
	}

	*flags |= TF_NORMALMAP;
	*flags &= ~(TF_INTENSITY | TF_ALPHA);

	*image = R_SmoothNormalMap(img, *width, *height, clamp);

	Mem_Free(img);

	return true;
}

/*
 ==================
 R_LoadImage
 ==================
*/
static bool R_LoadImage (script_t *script, byte **image, int *width, int *height, int *flags, bool clamp){

	token_t	token;

	if (!PS_ReadToken(script, &token)){
		Com_Printf(S_COLOR_YELLOW "WARNING: missing parameters for image program\n");
		return false;
	}

	if (!Str_ICompare(token.string, "add"))
		return R_ParseAdd(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "scale"))
		return R_ParseScale(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "invertColor"))
		return R_ParseInvertColor(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "invertAlpha"))
		return R_ParseInvertAlpha(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "makeIntensity"))
		return R_ParseMakeIntensity(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "makeLuminance"))
		return R_ParseMakeLuminance(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "makeAlpha"))
		return R_ParseMakeAlpha(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "heightMap"))
		return R_ParseHeightMap(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "addNormals"))
		return R_ParseAddNormals(script, image, width, height, flags, clamp);
	else if (!Str_ICompare(token.string, "smoothNormals"))
		return R_ParseSmoothNormals(script, image, width, height, flags, clamp);
	else {
		if (R_LoadTGA(token.string, image, width, height))
			return true;

		return false;
	}
}


/*
 ==============================================================================

 TEXTURE LOADING

 ==============================================================================
*/


/*
 ==================
 R_ResampleTexture
 ==================
*/
static void R_ResampleTexture (byte *in, int inWidth, int inHeight, byte *out, int outWidth, int outHeight, bool isNormalMap){

	uint	frac, fracStep;
	uint	p1[4096], p2[4096];
	uint	*row1, *row2;
	byte	*pix1, *pix2, *pix3, *pix4;
	odVec3	normal;
	int		i, x, y;

	fracStep = inWidth * 0x10000 / outWidth;

	frac = fracStep >> 2;
	for (i = 0; i < outWidth; i++){
		p1[i] = 4 * (frac >> 16);
		frac += fracStep;
	}

	frac = (fracStep >> 2) * 3;
	for (i = 0; i < outWidth; i++){
		p2[i] = 4 * (frac >> 16);
		frac += fracStep;
	}

	if (isNormalMap){
		for (y = 0; y < outHeight; y++){
			row1 = (uint *)in + inWidth * M_Ftol(((float)y + 0.25f) * inHeight/outHeight);
			row2 = (uint *)in + inWidth * M_Ftol(((float)y + 0.75f) * inHeight/outHeight);

			for (x = 0; x < outWidth; x++, out += 4){
				pix1 = (byte *)row1 + p1[x];
				pix2 = (byte *)row1 + p2[x];
				pix3 = (byte *)row2 + p1[x];
				pix4 = (byte *)row2 + p2[x];

				normal[0] = (pix1[0] * (1.0f/127) - 1.0f) + (pix2[0] * (1.0f/127) - 1.0f) + (pix3[0] * (1.0f/127) - 1.0f) + (pix4[0] * (1.0f/127) - 1.0f);
				normal[1] = (pix1[1] * (1.0f/127) - 1.0f) + (pix2[1] * (1.0f/127) - 1.0f) + (pix3[1] * (1.0f/127) - 1.0f) + (pix4[1] * (1.0f/127) - 1.0f);
				normal[2] = (pix1[2] * (1.0f/127) - 1.0f) + (pix2[2] * (1.0f/127) - 1.0f) + (pix3[2] * (1.0f/127) - 1.0f) + (pix4[2] * (1.0f/127) - 1.0f);

				if (!normal.Normalize())
					normal.Set(0.0f, 0.0f, 1.0f);

				out[0] = 128 + M_Ftol(127.0f * normal[0]);
				out[1] = 128 + M_Ftol(127.0f * normal[1]);
				out[2] = 128 + M_Ftol(127.0f * normal[2]);
				out[3] = 255;
			}
		}
	}
	else {
		for (y = 0; y < outHeight; y++){
			row1 = (uint *)in + inWidth * M_Ftol(((float)y + 0.25f) * inHeight/outHeight);
			row2 = (uint *)in + inWidth * M_Ftol(((float)y + 0.75f) * inHeight/outHeight);

			for (x = 0; x < outWidth; x++, out += 4){
				pix1 = (byte *)row1 + p1[x];
				pix2 = (byte *)row1 + p2[x];
				pix3 = (byte *)row2 + p1[x];
				pix4 = (byte *)row2 + p2[x];

				out[0] = (pix1[0] + pix2[0] + pix3[0] + pix4[0]) >> 2;
				out[1] = (pix1[1] + pix2[1] + pix3[1] + pix4[1]) >> 2;
				out[2] = (pix1[2] + pix2[2] + pix3[2] + pix4[2]) >> 2;
				out[3] = (pix1[3] + pix2[3] + pix3[3] + pix4[3]) >> 2;
			}
		}
	}
}

/*
 ==================
 R_BuildMipMap

 Operates in place, quartering the size of the texture
 ==================
*/
static void R_BuildMipMap (byte *in, int width, int height, bool isNormalMap){

	byte	*out = in;
	odVec3	normal;
	int		x, y;

	width <<= 2;
	height >>= 1;

	if (isNormalMap){
		for (y = 0; y < height; y++, in += width){
			for (x = 0; x < width; x += 8, in += 8, out += 4){
				normal[0] = (in[0] * (1.0f/127) - 1.0f) + (in[4] * (1.0f/127) - 1.0f) + (in[width+0] * (1.0f/127) - 1.0f) + (in[width+4] * (1.0f/127) - 1.0f);
				normal[1] = (in[1] * (1.0f/127) - 1.0f) + (in[5] * (1.0f/127) - 1.0f) + (in[width+1] * (1.0f/127) - 1.0f) + (in[width+5] * (1.0f/127) - 1.0f);
				normal[2] = (in[2] * (1.0f/127) - 1.0f) + (in[6] * (1.0f/127) - 1.0f) + (in[width+2] * (1.0f/127) - 1.0f) + (in[width+6] * (1.0f/127) - 1.0f);

				if (!normal.Normalize())
					normal.Set(0.0f, 0.0f, 1.0f);

				out[0] = 128 + M_Ftol(127.0f * normal[0]);
				out[1] = 128 + M_Ftol(127.0f * normal[1]);
				out[2] = 128 + M_Ftol(127.0f * normal[2]);
				out[3] = 255;
			}
		}
	}
	else {
		for (y = 0; y < height; y++, in += width){
			for (x = 0; x < width; x += 8, in += 8, out += 4){
				out[0] = (in[0] + in[4] + in[width+0] + in[width+4]) >> 2;
				out[1] = (in[1] + in[5] + in[width+1] + in[width+5]) >> 2;
				out[2] = (in[2] + in[6] + in[width+2] + in[width+6]) >> 2;
				out[3] = (in[3] + in[7] + in[width+3] + in[width+7]) >> 2;
			}
		}
	}
}

/*
 ==================
 R_CalcTextureSamples
 ==================
*/
static int R_CalcTextureSamples (byte **image, int width, int height, bool isCubeMap){

	byte	*scan;
	bool	hasColor = false, hasAlpha = false;
	int		faces, size;
	int		i, j;

	if (isCubeMap)
		faces = 6;
	else
		faces = 1;

	size = width * height;

	for (i = 0; i < faces; i++){
		scan = image[i];

		for (j = 0; j < size; j++, scan += 4){
			if (scan[0] != scan[1] || scan[1] != scan[2])
				hasColor = true;

			if (scan[3] != 255)
				hasAlpha = true;

			if (hasColor && hasAlpha)
				break;
		}

		if (hasColor && hasAlpha)
			break;
	}

	if (hasColor)
		return (hasAlpha) ? 4 : 3;
	else
		return (hasAlpha) ? 2 : 1;
}

/*
 ==================
 R_CalcTextureSize
 ==================
*/
static int R_CalcTextureSize (uint target, int level, int width, int height, int samples, bool useCompression){

	int		size, isCompressed;

	if (useCompression){
		qglGetTexLevelParameteriv(target, level, GL_TEXTURE_COMPRESSED, &isCompressed);
		if (isCompressed){
			qglGetTexLevelParameteriv(target, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &size);
			return size;
		}
	}

	if (samples >= 3)
		size = width * height * 4;
	else
		size = width * height * samples;

	return size;
}

/*
 ==================
 R_UploadTexture
 ==================
*/
static void R_UploadTexture (byte **image, int width, int height, texture_t *texture){

	odVec4	zeroBorder(0.0f, 0.0f, 0.0f, 1.0f), alphaZeroBorder(1.0f, 1.0f, 1.0f, 0.0f);
	byte	*texImage;
	uint	texTarget;
	bool	useCompression;
	int		mipLevel, mipWidth, mipHeight;
	int		i;

	// Find nearest power of two, rounding down if desired
	if (r_roundTexturesDown->integerValue){
		texture->width = M_FloorPowerOfTwo(width);
		texture->height = M_FloorPowerOfTwo(height);
	}
	else {
		texture->width = M_CeilPowerOfTwo(width);
		texture->height = M_CeilPowerOfTwo(height);
	}

	// Down size if desired
	if (r_maxTextureSize->integerValue > 0 && !(texture->flags & TF_NOPICMIP)){
		while (texture->width > r_maxTextureSize->integerValue || texture->height > r_maxTextureSize->integerValue){
			texture->width >>= 1;
			texture->height >>= 1;
		}
	}

	// Clamp to hardware limits
	if (texture->isCubeMap){
		while (texture->width > glConfig.maxCubeMapTextureSize || texture->height > glConfig.maxCubeMapTextureSize){
			texture->width >>= 1;
			texture->height >>= 1;
		}
	}
	else {
		while (texture->width > glConfig.maxTextureSize || texture->height > glConfig.maxTextureSize){
			texture->width >>= 1;
			texture->height >>= 1;
		}
	}

	if (texture->width < 1)
		texture->width = 1;
	if (texture->height < 1)
		texture->height = 1;

	// Determine if it should be compressed
	if (texture->flags & TF_UNCOMPRESSED)
		useCompression = false;
	else {
		if (texture->flags & TF_NORMALMAP)
			useCompression = (r_compressTextures->integerValue >= 2);
		else
			useCompression = (r_compressTextures->integerValue >= 1);
	}

	// Set texture format
	if (texture->flags & (TF_INTENSITY | TF_ALPHA))
		texture->samples = 1;

	if (useCompression){
		switch (texture->samples){
		case 1:
			texture->format = GL_COMPRESSED_LUMINANCE;
			break;
		case 2:
			texture->format = GL_COMPRESSED_LUMINANCE_ALPHA;
			break;
		case 3:
			texture->format = GL_COMPRESSED_RGB;
			break;
		case 4:
			texture->format = GL_COMPRESSED_RGBA;
			break;
		}

		if (texture->flags & TF_INTENSITY)
			texture->format = GL_COMPRESSED_INTENSITY;
		if (texture->flags & TF_ALPHA)
			texture->format = GL_COMPRESSED_ALPHA;
	}
	else {
		switch (texture->samples){
		case 1:
			texture->format = GL_LUMINANCE8;
			break;
		case 2:
			texture->format = GL_LUMINANCE8_ALPHA8;
			break;
		case 3:
			texture->format = GL_RGB8;
			break;
		case 4:
			texture->format = GL_RGBA8;
			break;
		}

		if (texture->flags & TF_INTENSITY)
			texture->format = GL_INTENSITY8;
		if (texture->flags & TF_ALPHA)
			texture->format = GL_ALPHA8;
	}

	// Set texture target
	if (texture->isCubeMap){
		texture->target = GL_TEXTURE_CUBE_MAP;

		texTarget = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
	}
	else {
		texture->target = GL_TEXTURE_2D;

		texTarget = GL_TEXTURE_2D;
	}

	// Bind the texture
	qglGenTextures(1, &texture->textureId);

	GL_BindTexture(texture);

	// Upload all the faces
	for (i = 0; i < 6; i++){
		// Copy or resample the texture
		if (texture->width == width && texture->height == height)
			texImage = image[i];
		else {
			texImage = (byte *)Mem_Alloc(texture->width * texture->height * 4, TAG_TEMP);

			R_ResampleTexture(image[i], width, height, texImage, texture->width, texture->height, (texture->flags & TF_NORMALMAP));
		}

		// Upload the base texture
		qglTexImage2D(texTarget + i, 0, texture->format, texture->width, texture->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, texImage);

		// Calculate size
		texture->size += R_CalcTextureSize(texTarget + i, 0, texture->width, texture->height, texture->samples, useCompression);

		// Generate mipmaps if desired
		if (texture->filter == TF_DEFAULT){
			mipLevel = 0;
			mipWidth = texture->width;
			mipHeight = texture->height;

			while (mipWidth > 1 || mipHeight > 1){
				// Development tool
				if (r_colorMipLevels->integerValue){
					if (!(texture->flags & (TF_INTERNAL | TF_LIGHTMAP | TF_NORMALMAP))){
						qglPixelTransferf(GL_RED_BIAS, (mipLevel % 3 == 0) ? 1.0f : 0.0f);
						qglPixelTransferf(GL_GREEN_BIAS, (mipLevel % 3 == 1) ? 1.0f : 0.0f);
						qglPixelTransferf(GL_BLUE_BIAS, (mipLevel % 3 == 2) ? 1.0f : 0.0f);
					}
				}

				// Build the mipmap
				R_BuildMipMap(texImage, mipWidth, mipHeight, (texture->flags & TF_NORMALMAP));

				mipLevel++;

				mipWidth >>= 1;
				if (mipWidth < 1)
					mipWidth = 1;

				mipHeight >>= 1;
				if (mipHeight < 1)
					mipHeight = 1;

				// Upload the mipmap texture
				qglTexImage2D(texTarget + i, mipLevel, texture->format, mipWidth, mipHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, texImage);

				// Calculate size
				texture->size += R_CalcTextureSize(texTarget + i, mipLevel, mipWidth, mipHeight, texture->samples, useCompression);
			}

			if (r_colorMipLevels->integerValue){
				if (!(texture->flags & (TF_INTERNAL | TF_LIGHTMAP | TF_NORMALMAP))){
					qglPixelTransferf(GL_RED_BIAS, 0.0f);
					qglPixelTransferf(GL_GREEN_BIAS, 0.0f);
					qglPixelTransferf(GL_BLUE_BIAS, 0.0f);
				}
			}
		}

		// Free texture data
		if (texImage != image[i])
			Mem_Free(texImage);

		// 2D textures have only one face
		if (!texture->isCubeMap)
			break;
	}

	// Set texture filter
	switch (texture->filter){
	case TF_DEFAULT:
		qglTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, r_textureMinFilter);
		qglTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, r_textureMagFilter);

		// Set texture LOD bias
		qglTexParameterf(texture->target, GL_TEXTURE_LOD_BIAS, r_textureLODBias->floatValue);

		// Set texture anisotropy if available
		if (glConfig.textureFilterAnisotropicAvailable)
			qglTexParameterf(texture->target, GL_TEXTURE_MAX_ANISOTROPY_EXT, r_textureAnisotropy->floatValue);

		break;
	case TF_NEAREST:
		qglTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		qglTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

		break;
	case TF_LINEAR:
		qglTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		qglTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		break;
	default:
		Com_Error(false, "R_UploadTexture: bad texture filter (%i)", texture->filter);
	}

	// Set texture wrap
	switch (texture->wrap){
	case TW_REPEAT:
		qglTexParameteri(texture->target, GL_TEXTURE_WRAP_S, GL_REPEAT);
		qglTexParameteri(texture->target, GL_TEXTURE_WRAP_T, GL_REPEAT);

		break;
	case TW_CLAMP:
		qglTexParameteri(texture->target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		qglTexParameteri(texture->target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		break;
	case TW_CLAMP_TO_ZERO:
		qglTexParameteri(texture->target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
		qglTexParameteri(texture->target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

		qglTexParameterfv(texture->target, GL_TEXTURE_BORDER_COLOR, zeroBorder.ToFloatPtr());

		break;
	case TW_CLAMP_TO_ZERO_ALPHA:
		qglTexParameteri(texture->target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
		qglTexParameteri(texture->target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

		qglTexParameterfv(texture->target, GL_TEXTURE_BORDER_COLOR, alphaZeroBorder.ToFloatPtr());

		break;
	default:
		Com_Error(false, "R_UploadTexture: bad texture wrap (%i)", texture->wrap);
	}
}

/*
 ==================
 R_LoadTexture
 ==================
*/
static texture_t *R_LoadTexture (const char *name, byte *image, int width, int height, int samples, int flags, textureFilter_t filter, textureWrap_t wrap){

	texture_t	*texture;
	uint		hashKey;

	if (r_numTextures == MAX_TEXTURES)
		Com_Error(false, "R_LoadTexture: MAX_TEXTURES hit");

	r_textures[r_numTextures++] = texture = (texture_t *)Mem_Alloc(sizeof(texture_t), TAG_RENDERER);

	// Fill it in
	Str_Copy(texture->name, name, sizeof(texture->name));
	texture->flags = flags;
	texture->filter = filter;
	texture->wrap = wrap;
	texture->isCubeMap = false;
	texture->samples = samples;
	texture->size = 0;
	texture->frameCount = 0;

	R_UploadTexture(&image, width, height, texture);

	// Add to hash table
	hashKey = Str_HashKey(texture->name, TEXTURES_HASH_SIZE, false);

	texture->nextHash = r_texturesHashTable[hashKey];
	r_texturesHashTable[hashKey] = texture;

	return texture;
}

/*
 ==================
 R_LoadCubeMapTexture
 ==================
*/
static texture_t *R_LoadCubeMapTexture (const char *name, byte **images, int width, int height, int samples, int flags, textureFilter_t filter, textureWrap_t wrap){

	texture_t	*texture;
	uint		hashKey;

	if (r_numTextures == MAX_TEXTURES)
		Com_Error(false, "R_LoadCubeMapTexture: MAX_TEXTURES hit");

	r_textures[r_numTextures++] = texture = (texture_t *)Mem_Alloc(sizeof(texture_t), TAG_RENDERER);

	// Fill it in
	Str_Copy(texture->name, name, sizeof(texture->name));
	texture->flags = flags;
	texture->filter = filter;
	texture->wrap = wrap;
	texture->isCubeMap = true;
	texture->samples = samples;
	texture->size = 0;
	texture->frameCount = 0;

	R_UploadTexture(images, width, height, texture);

	// Add to hash table
	hashKey = Str_HashKey(texture->name, TEXTURES_HASH_SIZE, false);

	texture->nextHash = r_texturesHashTable[hashKey];
	r_texturesHashTable[hashKey] = texture;

	return texture;
}

/*
 ==================
 R_FindTexture
 ==================
*/
texture_t *R_FindTexture (const char *name, int flags, textureFilter_t filter, textureWrap_t wrap){

	texture_t	*texture;
	script_t	*script;
	byte		*image, internalImage[16*16*4];
	int			width, height, samples;
	uint		hashKey;
	int			i;

	// See if already loaded
	hashKey = Str_HashKey(name, TEXTURES_HASH_SIZE, false);

	for (texture = r_texturesHashTable[hashKey]; texture; texture = texture->nextHash){
		if (texture->isCubeMap)
			continue;

		if (!Str_ICompare(texture->name, name)){
			if (texture->flags & TF_INTERNAL)
				return texture;

			if (texture->flags != flags)
				Com_Printf(S_COLOR_YELLOW "WARNING: reused texture '%s' with mixed flags parameter\n", name);
			if (texture->filter != filter)
				Com_Printf(S_COLOR_YELLOW "WARNING: reused texture '%s' with mixed filter parameter\n", name);
			if (texture->wrap != wrap)
				Com_Printf(S_COLOR_YELLOW "WARNING: reused texture '%s' with mixed wrap parameter\n", name);

			return texture;
		}
	}

	// If name begins with a "_", create an empty internal image
	if (name[0] == '_' && name[1] != 0){
		for (i = 0; i < 256; i++)
			((dword *)internalImage)[i] = LittleLong(0xFFFFFFFF);

		return R_LoadTexture(name, internalImage, 16, 16, 4, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | flags, (filter != TF_DEFAULT) ? filter : TF_LINEAR, wrap);
	}

	// Load it from disk
	script = PS_LoadScriptMemory("ImageProgram", name, Str_Length(name));
	if (!script)
		return NULL;

	PS_SetScriptFlags(script, SF_NOWARNINGS | SF_NOERRORS | SF_ALLOWPATHNAMES);

	if (!R_LoadImage(script, &image, &width, &height, &flags, (wrap != TW_REPEAT))){
		PS_FreeScript(script);
		return NULL;
	}

	PS_FreeScript(script);

	// Calculate samples
	samples = R_CalcTextureSamples(&image, width, height, false);

	// Load the texture
	texture = R_LoadTexture(name, image, width, height, samples, flags, filter, wrap);

	Mem_Free(image);

	return texture;
}

/*
 ==================
 R_FindCubeMapTexture
 ==================
*/
texture_t *R_FindCubeMapTexture (const char *name, int flags, textureFilter_t filter, textureWrap_t wrap, bool cameraSpace){

	texture_t	*texture;
	char		realName[MAX_OSPATH];
	byte		*image;
	int			width, height;
	byte		*cubeImages[6];
	int			cubeWidth, cubeHeight, cubeSamples;
	uint		hashKey;
	int			i, j;

	// See if already loaded
	hashKey = Str_HashKey(name, TEXTURES_HASH_SIZE, false);

	for (texture = r_texturesHashTable[hashKey]; texture; texture = texture->nextHash){
		if (!texture->isCubeMap)
			continue;

		if (!Str_ICompare(texture->name, name)){
			if (texture->flags & TF_INTERNAL)
				return texture;

			if (texture->flags != flags)
				Com_Printf(S_COLOR_YELLOW "WARNING: reused texture '%s' with mixed flags parameter\n", name);
			if (texture->filter != filter)
				Com_Printf(S_COLOR_YELLOW "WARNING: reused texture '%s' with mixed filter parameter\n", name);
			if (texture->wrap != wrap)
				Com_Printf(S_COLOR_YELLOW "WARNING: reused texture '%s' with mixed wrap parameter\n", name);

			return texture;
		}
	}

	// Load it from disk
	for (i = 0; i < 6; i++){
		Str_SPrintf(realName, sizeof(realName), "%s_%s.tga", name, r_cubeSides[cameraSpace][i].suffix);
		if (!R_LoadTGA(realName, &image, &width, &height))
			break;

		// Check dimensions
		if (width != height){
			Com_Printf(S_COLOR_YELLOW "WARNING: cube map face '%s_%s' is not square (%i != %i)\n", name, r_cubeSides[cameraSpace][i].suffix, width, height);
			break;
		}

		// Copy or flip
		if (!r_cubeSides[cameraSpace][i].flipX && !r_cubeSides[cameraSpace][i].flipY && !r_cubeSides[cameraSpace][i].flipDiagonal)
			cubeImages[i] = image;
		else {
			cubeImages[i] = R_FlipImage(image, width, height, 4, r_cubeSides[cameraSpace][i].flipX, r_cubeSides[cameraSpace][i].flipY, r_cubeSides[cameraSpace][i].flipDiagonal);

			Mem_Free(image);
		}

		// Compare dimensions with previous faces
		if (i != 0){
			if (width != cubeWidth || height != cubeHeight){
				Com_Printf(S_COLOR_YELLOW "WARNING: cube map face '%s_%s' has mismatched dimensions (%i x %i != %i x %i)\n", name, r_cubeSides[cameraSpace][i].suffix, width, height, cubeWidth, cubeHeight);
				break;
			}

			continue;
		}

		cubeWidth = width;
		cubeHeight = height;
	}

	if (i != 6){
		for (j = 0; j < i; j++)
			Mem_Free(cubeImages[j]);

		return NULL;
	}

	// Calculate samples
	cubeSamples = R_CalcTextureSamples(cubeImages, cubeWidth, cubeHeight, true);

	// Load the texture
	texture = R_LoadCubeMapTexture(name, cubeImages, cubeWidth, cubeHeight, cubeSamples, flags, filter, wrap);

	for (i = 0; i < 6; i++)
		Mem_Free(cubeImages[i]);

	return texture;
}


// ============================================================================


/*
 ==================
 R_GetTextureByName
 ==================
*/
texture_t *R_GetTextureByName (const char *name){

	texture_t	*texture;
	uint		hashKey;

	// See if already loaded
	hashKey = Str_HashKey(name, TEXTURES_HASH_SIZE, false);

	for (texture = r_texturesHashTable[hashKey]; texture; texture = texture->nextHash){
		if (!Str_ICompare(texture->name, name))
			return texture;
	}

	return NULL;
}

/*
 ==================
 R_SetTextureParameters
 ==================
*/
void R_SetTextureParameters (void){

	texture_t		*texture;
	filterTable_t	*filterTable;
	int				i;

	for (filterTable = r_filterTable; filterTable->name; filterTable++){
		if (!Str_ICompare(filterTable->name, r_textureFilter->value)){
			r_textureMinFilter = filterTable->min;
			r_textureMagFilter = filterTable->mag;

			break;
		}
	}

	if (!filterTable->name){
		CVar_SetString(r_textureFilter, "GL_LINEAR_MIPMAP_LINEAR");

		r_textureMinFilter = GL_LINEAR_MIPMAP_LINEAR;
		r_textureMagFilter = GL_LINEAR;
	}

	r_textureFilter->modified = false;

	if (r_textureLODBias->floatValue < -glConfig.maxTextureLODBias)
		CVar_SetFloat(r_textureLODBias, -glConfig.maxTextureLODBias);
	else if (r_textureLODBias->floatValue > glConfig.maxTextureLODBias)
		CVar_SetFloat(r_textureLODBias, glConfig.maxTextureLODBias);

	r_textureLODBias->modified = false;

	if (glConfig.textureFilterAnisotropicAvailable){
		if (r_textureAnisotropy->floatValue < 1.0f)
			CVar_SetFloat(r_textureAnisotropy, 1.0f);
		else if (r_textureAnisotropy->floatValue > glConfig.maxTextureMaxAnisotropy)
			CVar_SetFloat(r_textureAnisotropy, glConfig.maxTextureMaxAnisotropy);
	}

	r_textureAnisotropy->modified = false;

	// Change all the existing mipmapped texture objects
	for (i = 0; i < r_numTextures; i++){
		texture = r_textures[i];

		if (texture->filter != TF_DEFAULT)
			continue;

		GL_BindTexture(texture);

		// Set texture filter
		qglTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, r_textureMinFilter);
		qglTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, r_textureMagFilter);

		// Set texture LOD bias
		qglTexParameterf(texture->target, GL_TEXTURE_LOD_BIAS, r_textureLODBias->floatValue);

		// Set texture anisotropy if available
		if (glConfig.textureFilterAnisotropicAvailable)
			qglTexParameterf(texture->target, GL_TEXTURE_MAX_ANISOTROPY_EXT, r_textureAnisotropy->floatValue);
	}
}

/*
 ==================
 R_ScreenShot_f
 ==================
*/
static void R_ScreenShot_f (void){

	char	name[MAX_OSPATH];
	byte	*buffer;
	bool	silent;
	int		i, size;

	if (Cmd_Argc() > 3){
		Com_Printf("Usage: screenShot [\"-silent\"] [name]\n");
		return;
	}

	if (Cmd_Argc() == 2){
		if (!Str_ICompare("-silent", Cmd_Argv(1))){
			silent = true;

			name[0] = 0;
		}
		else {
			silent = false;

			Str_SPrintf(name, sizeof(name), "screenshots/%s", Cmd_Argv(1));
			Str_DefaultFileExtension(name, sizeof(name), ".tga");
		}
	}
	else if (Cmd_Argc() == 3){
		if (!Str_ICompare("-silent", Cmd_Argv(1)))
			silent = true;
		else {
			Com_Printf("Usage: screenShot [\"-silent\"] [name]\n");
			return;
		}

		Str_SPrintf(name, sizeof(name), "screenshots/%s", Cmd_Argv(2));
		Str_DefaultFileExtension(name, sizeof(name), ".tga");
	}
	else {
		silent = false;

		name[0] = 0;
	}

	if (!name[0]){
		// Find a file name to save it to
		for (i = 0; i <= 9999; i++){
			Str_SPrintf(name, sizeof(name), "screenshots/shot%04i.tga", i);
			if (!FS_FileExists(name))
				break;
		} 

		if (i == 10000){
			Com_Printf("Screenshots directory is full!\n");
			return;
 		}
	}

	// Allocate the buffer
	size = 18 + (glConfig.videoWidth * glConfig.videoHeight * 3);
	buffer = (byte *)Mem_Alloc(size, TAG_TEMP);

	// Set up the header
	memset(buffer, 0, 18);

	buffer[2] = 2;
	buffer[12] = glConfig.videoWidth & 255;
	buffer[13] = glConfig.videoWidth >> 8;
	buffer[14] = glConfig.videoHeight & 255;
	buffer[15] = glConfig.videoHeight >> 8;
	buffer[16] = 24;

	// Read the pixels
	qglReadPixels(0, 0, glConfig.videoWidth, glConfig.videoHeight, GL_BGR, GL_UNSIGNED_BYTE, buffer + 18);

	// Apply gamma if needed
	if (glConfig.deviceSupportsGamma && (r_gamma->floatValue != 1.0 || r_brightness->floatValue != 1.0)){
		for (i = 18; i < size; i += 3){
			buffer[i+0] = tr.gammaTable[buffer[i+0]];
			buffer[i+1] = tr.gammaTable[buffer[i+1]];
			buffer[i+2] = tr.gammaTable[buffer[i+2]];
		}
	}

	// Write the image
	if (!FS_WriteFile(name, buffer, size)){
		if (!silent)
			Com_Printf("Couldn't write %s\n", name);

		Mem_Free(buffer);
		return;
	}

	Mem_Free(buffer);

	if (!silent)
		Com_Printf("Wrote %s\n", name);
}

/*
 ==================
 R_EnvShot_f
 ==================
*/
static void R_EnvShot_f (void){

	fileHandle_t	f;
	renderView_t	renderView;
	char			baseName[MAX_OSPATH], realName[MAX_OSPATH];
	byte			header[18];
	byte			*buffer, *pixels;
	int				i, size;

	if (Cmd_Argc() < 2 || Cmd_Argc() > 3){
		Com_Printf("Usage: envShot <baseName> [size]\n");
		return;
	}

	if (!tr.primaryViewAvailable){
		Com_Printf("No primaryView available\n");
		return;
	}

	Str_Copy(baseName, Cmd_Argv(1), sizeof(baseName));

	if (Cmd_Argc() == 3){
		size = Str_ToInteger(Cmd_Argv(2));

		if (size < 16)
			size = 16;
		else
			size = M_FloorPowerOfTwo(size);
	}
	else
		size = 256;

	// Make sure the specified size is valid
	if (size > glConfig.videoWidth || size > glConfig.videoHeight){
		Com_Printf("Specified size is greater than current resolution\n");
		return;
	}

	// Set up the header
	memset(header, 0, 18);

	header[2] = 2;
	header[12] = size & 255;
	header[13] = size >> 8;
	header[14] = size & 255;
	header[15] = size >> 8;
	header[16] = 24;

	// Render a frame for each face of the environment box
	tr.envShotRendering = true;
	tr.envShotSize = size;

	for (i = 0; i < 6; i++){
		// Set up renderView
		renderView = tr.primaryView.renderView;

		renderView.x = 0;
		renderView.y = 0;
		renderView.width = SCREEN_WIDTH;
		renderView.height = SCREEN_HEIGHT;

		renderView.fovX = 90.0f;
		renderView.fovY = 90.0f;

		renderView.axis = r_envShot[i].angles.ToMat3();

		// Render the scene
		R_BeginFrame(tr.frameTime);
		R_RenderScene(tr.primaryView.scene, &renderView, true);
		R_EndFrame();

		// Allocate the buffer
		pixels = (byte *)Mem_Alloc(size * size * 3, TAG_TEMP);

		// Read the pixels
		qglReadPixels(0, 0, size, size, GL_BGR, GL_UNSIGNED_BYTE, pixels);

		// Copy or flip
		if (!r_envShot[i].flipX && !r_envShot[i].flipY && !r_envShot[i].flipDiagonal)
			buffer = pixels;
		else {
			buffer = R_FlipImage(pixels, size, size, 3, r_envShot[i].flipX, r_envShot[i].flipY, r_envShot[i].flipDiagonal);

			Mem_Free(pixels);
		}

		// Write the image
		Str_SPrintf(realName, sizeof(realName), "env/%s_%s.tga", baseName, r_envShot[i].suffix);

		FS_OpenFile(realName, &f, FS_WRITE);
		if (!f){
			Com_Printf("Couldn't write env/%s_*.tga\n", baseName);

			Mem_Free(buffer);
			return;
		}

		FS_Write(header, sizeof(header), f);
		FS_Write(buffer, size * size * 3, f);
		FS_CloseFile(f);

		Mem_Free(buffer);
	}

	tr.envShotRendering = false;

	Com_Printf("Wrote env/%s_*.tga\n", baseName);
}

/*
 ==================
 R_ListTextures_f
 ==================
*/
static void R_ListTextures_f (void){

	texture_t	*texture;
	int			bytes = 0;
	int			i;

	Com_Printf("\n");
	Com_Printf("      -w-- -h-- -size- -fmt- type filt wrap -name--------\n");

	for (i = 0; i < r_numTextures; i++){
		texture = r_textures[i];

		bytes += texture->size;

		Com_Printf("%4i: ", i);

		Com_Printf("%4i %4i ", texture->width, texture->height);

		Com_Printf("%5ik ", texture->size >> 10);

		switch (texture->format){
		case GL_COMPRESSED_RGBA:
			Com_Printf("CRGBA ");
			break;
		case GL_COMPRESSED_RGB:
			Com_Printf("CRGB  ");
			break;
		case GL_COMPRESSED_LUMINANCE_ALPHA:
			Com_Printf("CLA   ");
			break;
		case GL_COMPRESSED_LUMINANCE:
			Com_Printf("CL    ");
			break;
		case GL_COMPRESSED_ALPHA:
			Com_Printf("CA    ");
			break;
		case GL_COMPRESSED_INTENSITY:
			Com_Printf("CI    ");
			break;
		case GL_RGBA8:
			Com_Printf("RGBA8 ");
			break;
		case GL_RGB8:
			Com_Printf("RGB8  ");
			break;
		case GL_LUMINANCE8_ALPHA8:
			Com_Printf("L8A8  ");
			break;
		case GL_LUMINANCE8:
			Com_Printf("L8    ");
			break;
		case GL_ALPHA8:
			Com_Printf("A8    ");
			break;
		case GL_INTENSITY8:
			Com_Printf("I8    ");
			break;
		default:
			Com_Printf("????? ");
			break;
		}

		switch (texture->target){
		case GL_TEXTURE_2D:
			Com_Printf(" 2D  ");
			break;
		case GL_TEXTURE_CUBE_MAP:
			Com_Printf("CUBE ");
			break;
		default:
			Com_Printf("???? ");
			break;
		}

		switch (texture->filter){
		case TF_DEFAULT:
			Com_Printf("dflt ");
			break;
		case TF_NEAREST:
			Com_Printf("nrst ");
			break;
		case TF_LINEAR:
			Com_Printf("linr ");
			break;
		default:
			Com_Printf("???? ");
			break;
		}

		switch (texture->wrap){
		case TW_REPEAT:
			Com_Printf("rept ");
			break;
		case TW_CLAMP:
			Com_Printf("clmp ");
			break;
		case TW_CLAMP_TO_ZERO:
			Com_Printf("zero ");
			break;
		case TW_CLAMP_TO_ZERO_ALPHA:
			Com_Printf("azro ");
			break;
		default:
			Com_Printf("???? ");
			break;
		}

		Com_Printf("%s\n", texture->name);
	}

	Com_Printf("---------------------------------------------------------\n");
	Com_Printf("%i total textures\n", r_numTextures);
	Com_Printf("%.2f MB of texture data\n", bytes * (1.0f/1048576));
	Com_Printf("\n");
}

/*
 ==================
 R_CreateBuiltInTextures
 ==================
*/
static void R_CreateBuiltInTextures (void){

	byte	image[128*128*4];
	byte	*out;
	float	s, t, value;
	byte	intensity;
	int		i, x, y;

	// Default texture
	for (y = 0; y < 16; y++){
		for (x = 0; x < 16; x++){
			if (x == 0 || x == 15 || y == 0 || y == 15)
				((dword *)image)[y*16+x] = LittleLong(0xFFFFFFFF);
			else
				((dword *)image)[y*16+x] = LittleLong(0xFF000000);
		}
	}

	tr.defaultTexture = R_LoadTexture("_default", image, 16, 16, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_DEFAULT, TW_REPEAT);

	// White texture
	for (i = 0; i < 64; i++)
		((dword *)image)[i] = LittleLong(0xFFFFFFFF);

	tr.whiteTexture = R_LoadTexture("_white", image, 8, 8, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_DEFAULT, TW_REPEAT);

	// Black texture
	for (i = 0; i < 64; i++)
		((dword *)image)[i] = LittleLong(0xFF000000);

	tr.blackTexture = R_LoadTexture("_black", image, 8, 8, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_DEFAULT, TW_REPEAT);

	// Flat texture
	for (i = 0; i < 64; i++)
		((dword *)image)[i] = LittleLong(0xFFFF8080);

	tr.flatTexture = R_LoadTexture("_flat", image, 8, 8, 3, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | TF_NORMALMAP, TF_DEFAULT, TW_REPEAT);

	// Attenuation texture
	out = image;

	for (y = 0; y < 128; y++){
		for (x = 0; x < 128; x++, out += 4){
			s = (((float)x + 0.5f) * (2.0f/128) - 1.0f) * (1.0f/0.9375f);
			t = (((float)y + 0.5f) * (2.0f/128) - 1.0f) * (1.0f/0.9375f);

			value = 1.0f - M_Sqrt(s*s + t*t);

			intensity = M_ClampByte(M_Ftol(value * 255.0f));

			out[0] = intensity;
			out[1] = intensity;
			out[2] = intensity;
			out[3] = 255;
		}
	}

	tr.attenuationTexture = R_LoadTexture("_attenuation", image, 128, 128, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | TF_LIGHTMAP, TF_DEFAULT, TW_CLAMP_TO_ZERO);

	// No attenuation texture
	out = image;

	for (y = 0; y < 128; y++){
		for (x = 0; x < 128; x++, out += 4){
			if (x == 0 || x == 127 || y == 0 || y == 127)
				*(dword *)out = LittleLong(0xFF000000);
			else
				*(dword *)out = LittleLong(0xFFFFFFFF);
		}
	}

	tr.noAttenuationTexture = R_LoadTexture("_noAttenuation", image, 128, 128, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | TF_LIGHTMAP, TF_DEFAULT, TW_CLAMP_TO_ZERO);

	// Falloff texture
	out = image;

	for (y = 0; y < 8; y++){
		for (x = 0; x < 64; x++, out += 4){
			s = (((float)x + 0.5f) * (2.0f/64) - 1.0f) * (1.0f/0.9375f);

			value = 1.0f - M_Sqrt(s*s);

			intensity = M_ClampByte(M_Ftol(value * 255.0f));

			out[0] = intensity;
			out[1] = intensity;
			out[2] = intensity;
			out[3] = intensity;
		}
	}

	tr.falloffTexture = R_LoadTexture("_falloff", image, 64, 8, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | TF_INTENSITY | TF_LIGHTMAP, TF_LINEAR, TW_CLAMP);

	// No falloff texture
	out = image;

	for (y = 0; y < 8; y++){
		for (x = 0; x < 64; x++, out += 4){
			if (x == 0 || x == 63)
				*(dword *)out = LittleLong(0x00000000);
			else
				*(dword *)out = LittleLong(0xFFFFFFFF);
		}
	}

	tr.noFalloffTexture = R_LoadTexture("_noFalloff", image, 64, 8, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | TF_INTENSITY | TF_LIGHTMAP, TF_LINEAR, TW_CLAMP);

	// Fog texture
	out = image;

	for (y = 0; y < 128; y++){
		for (x = 0; x < 128; x++, out += 4){
			s = (((float)x + 0.5f) * (2.0f/128) - 1.0f) * (1.0f/0.9375f);
			t = (((float)y + 0.5f) * (2.0f/128) - 1.0f) * (1.0f/0.9375f);

			value = M_Pow(M_Sqrt(s*s + t*t), 0.5f);

			intensity = M_ClampByte(M_Ftol(value * 255.0f));

			out[0] = 255;
			out[1] = 255;
			out[2] = 255;
			out[3] = intensity;
		}
	}

	tr.fogTexture = R_LoadTexture("_fog", image, 128, 128, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | TF_ALPHA | TF_LIGHTMAP, TF_LINEAR, TW_CLAMP);

	// Fog enter texture
	out = image;

	for (y = 0; y < 64; y++){
		for (x = 0; x < 64; x++, out += 4){
			s = (((float)x + 0.5f) * (2.0f/64) - 1.0f) * (1.0f/0.3750f);
			t = (((float)y + 0.5f) * (2.0f/64) - 1.0f) * (1.0f/0.3750f);

			s = M_ClampFloat(s + (1.0f/16), -1.0f, 0.0f);
			t = M_ClampFloat(t + (1.0f/16), -1.0f, 0.0f);

			value = M_Pow(M_Sqrt(s*s + t*t), 0.5f);

			intensity = M_ClampByte(M_Ftol(value * 255.0f));

			out[0] = 255;
			out[1] = 255;
			out[2] = 255;
			out[3] = intensity;
		}
	}

	tr.fogEnterTexture = R_LoadTexture("_fogEnter", image, 64, 64, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | TF_ALPHA | TF_LIGHTMAP, TF_LINEAR, TW_CLAMP);

	// Quadratic texture
	out = image;

	for (y = 0; y < 8; y++){
		for (x = 0; x < 32; x++, out += 4){
			s = (((float)x + 0.5f) * (2.0f/32) - 1.0f);

			s = M_Fabs(s) - (1.0f/32);

			value = 1.0f - (s * 2.0f) + (s * s);

			intensity = M_ClampByte(M_Ftol(value * 255.0f));

			out[0] = intensity;
			out[1] = intensity;
			out[2] = intensity;
			out[3] = intensity;
		}
	}

	tr.quadraticTexture = R_LoadTexture("_quadratic", image, 32, 8, 1, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED | TF_INTENSITY, TF_LINEAR, TW_CLAMP);

	// Cinematic texture
	for (i = 0; i < 256; i++)
		((dword *)image)[i] = LittleLong(0xFFFFFFFF);

	tr.cinematicTexture = R_LoadTexture("_cinematic", image, 16, 16, 3, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_LINEAR, TW_CLAMP);

	// Portal sky texture
	for (i = 0; i < 256; i++)
		((dword *)image)[i] = LittleLong(0xFFFFFFFF);

	tr.portalSkyTexture = R_LoadTexture("_portalSky", image, 16, 16, 3, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_LINEAR, TW_CLAMP);

	// Subview texture
	for (i = 0; i < 256; i++)
		((dword *)image)[i] = LittleLong(0xFFFFFFFF);

	tr.subviewTexture = R_LoadTexture("_subview", image, 16, 16, 3, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_LINEAR, TW_CLAMP);

	// Bloom texture
	for (i = 0; i < 256; i++)
		((dword *)image)[i] = LittleLong(0xFFFFFFFF);

	tr.bloomTexture = R_LoadTexture("_bloom", image, 16, 16, 3, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_LINEAR, TW_CLAMP);

	// Scratch texture
	for (i = 0; i < 256; i++)
		((dword *)image)[i] = LittleLong(0xFFFFFFFF);

	tr.scratchTexture = R_LoadTexture("_scratch", image, 16, 16, 4, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_LINEAR, TW_REPEAT);

	// Accum texture
	for (i = 0; i < 256; i++)
		((dword *)image)[i] = LittleLong(0xFFFFFFFF);

	tr.accumTexture = R_LoadTexture("_accum", image, 16, 16, 3, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_LINEAR, TW_CLAMP);

	// Current render texture
	for (i = 0; i < 256; i++)
		((dword *)image)[i] = LittleLong(0xFFFFFFFF);

	tr.currentRenderTexture = R_LoadTexture("_currentRender", image, 16, 16, 3, TF_INTERNAL | TF_NOPICMIP | TF_UNCOMPRESSED, TF_LINEAR, TW_CLAMP);
}

/*
 ==================
 R_InitTextures
 ==================
*/
void R_InitTextures (void){

	float	f;
	int		i;

	// Register our commands
	Cmd_AddCommand("screenShot", R_ScreenShot_f, "Takes a screenshot");
	Cmd_AddCommand("envShot", R_EnvShot_f, "Takes an environment shot");
	Cmd_AddCommand("listTextures", R_ListTextures_f, "Lists loaded textures");

	// Build luminance table
	for (i = 0; i < 256; i++){
		f = (float)i;

		r_luminanceTable[i][0] = f * 0.299f;
		r_luminanceTable[i][1] = f * 0.587f;
		r_luminanceTable[i][2] = f * 0.114f;
	}

	// Set texture parameters
	R_SetTextureParameters();

	// Create built-in textures
	R_CreateBuiltInTextures();
}

/*
 ==================
 R_ShutdownTextures
 ==================
*/
void R_ShutdownTextures (void){

	texture_t	*texture;
	int			i;

	// Unregister our commands
	Cmd_RemoveCommand("screenShot");
	Cmd_RemoveCommand("envShot");
	Cmd_RemoveCommand("listTextures");

	// Delete all textures
	for (i = MAX_TEXTURE_UNITS - 1; i >= 0; i--){
		if (i >= glConfig.maxTextureUnits && i >= glConfig.maxCombinedTextureImageUnits)
			continue;

		qglActiveTexture(GL_TEXTURE0 + i);

		qglBindTexture(GL_TEXTURE_2D, 0);
		qglBindTexture(GL_TEXTURE_CUBE_MAP, 0);
	}

	for (i = 0; i < r_numTextures; i++){
		texture = r_textures[i];

		qglDeleteTextures(1, &texture->textureId);
	}

	// Clear texture list
	memset(r_texturesHashTable, 0, sizeof(r_texturesHashTable));
	memset(r_textures, 0, sizeof(r_textures));

	r_numTextures = 0;
}
